/**
 * Copyright (C) 2021 - 2031 O-Cubes Co., Ltd.
 */

/****************************************************************
 *  @file    dw_wdt.c
 *  @brief   Designware watchdog driver
 *  @version v1.0
 *  @date    03. Apr. 2023
 ****************************************************************/

#include <limits.h>

#include "io.h"
#include "common.h"
#include "dw_wdt.h"
#include "mem_map_table.h"
#include "crg.h"

static enum wdt_port g_wdt = eDW_WDT_0;
static DEV_CALLBACK wdt_callback;
static uint8_t wdt_irq_flag = 0;

#define WDT_BASE_ADDR                       MEM_MAP_WDT0_BASE_ADDR
#define WDT_REG_SIZE                        0x1000
#define WDT_OFF(member)                     (GET_OFFSET_OF_ADDR(WDT_BASE_ADDR + g_wdt * WDT_REG_SIZE, struct dw_wdt, member))
#define MSEC_PER_SEC                        1000
#define WDT_NUM_TOPS                        16
#define WDT_FIX_TOP(idx)                    (1U << (16 + idx))
#define WDT_TIMEOUT_RANGE_TOPINIT_SHIFT     4

static const uint32_t wdt_fix_tops[WDT_NUM_TOPS] =
{
	WDT_FIX_TOP(0), WDT_FIX_TOP(1), WDT_FIX_TOP(2),
	WDT_FIX_TOP(3), WDT_FIX_TOP(4), WDT_FIX_TOP(5),
	WDT_FIX_TOP(6), WDT_FIX_TOP(7), WDT_FIX_TOP(8),
	WDT_FIX_TOP(9), WDT_FIX_TOP(10), WDT_FIX_TOP(11),
	WDT_FIX_TOP(12), WDT_FIX_TOP(13), WDT_FIX_TOP(14),
	WDT_FIX_TOP(15)
};

struct dw_wdt_timeout {
	uint32_t top_val;
	uint32_t sec;
	uint32_t msec;
};

static struct dw_wdt_timeout g_wdt_timeouts[eDW_WDT_MAX][WDT_NUM_TOPS];
static enum dw_wdt_rmod g_wdt_rmod[eDW_WDT_MAX] = { DW_WDT_RMOD_IRQ };
static int g_wdt_pretimeout[eDW_WDT_MAX] = { 0 };
#define wdt_timeouts g_wdt_timeouts[g_wdt]
#define wdt_rmod g_wdt_rmod[g_wdt]
#define wdt_pretimeout g_wdt_pretimeout[g_wdt];

static inline uint32_t wdt_is_enabled(void)
{
	uint32_t reg = WDT_OFF(wdt_cr);
	uint32_t val;

	val = readl(reg);
	return val & DW_WDT_CR_EN;
}

static void wdt_update_mode(enum dw_wdt_rmod rmod)
{
	uint32_t reg = WDT_OFF(wdt_cr);
	uint32_t val;

	val = readl(reg);

	if (rmod == DW_WDT_RMOD_IRQ)
		val |= DW_WDT_CR_RMOD;
	else
		val &= ~DW_WDT_CR_RMOD;

	writel(val, reg);

	g_wdt_rmod[g_wdt] = rmod;
}

static uint32_t dw_wdt_find_best_top(uint32_t timeout, uint32_t *top_val)
{
	uint32_t idx;

	/*
	 * find a TOP with timeout greater or equal to the requested number.
	 * Note we'll select a TOP with maximum timeout if the requested
	 * timeout couldn't be reached.
	 */
	for (idx = 0; idx < WDT_NUM_TOPS; ++idx) {
		if (wdt_timeouts[idx].sec >= timeout)
			break;
	}

	if (idx == WDT_NUM_TOPS)
		--idx;

	*top_val = wdt_timeouts[idx].top_val;

	return wdt_timeouts[idx].sec;
}

static uint32_t wdt_set_timeout(uint32_t top_s)
{
	uint32_t reg;
	uint32_t timeout;
	uint32_t top_val;

	/*
	 * Note IRQ mode being enabled means having a non-zero pre-timeout
	 * setup. In this case we try to find a TOP as close to the half of the
	 * requested timeout as possible since DW Watchdog IRQ mode is designed
	 * in two stages way - first timeout rises the pre-timeout interrupt,
	 * second timeout performs the system reset. So basically the effective
	 * watchdog-caused reset happens after two watchdog TOPs elapsed.
	 */
	timeout = dw_wdt_find_best_top(DIV_ROUND_UP(top_s, wdt_rmod), &top_val);

	if (wdt_rmod == DW_WDT_RMOD_IRQ)
		g_wdt_pretimeout[g_wdt] = timeout;
	else
		g_wdt_pretimeout[g_wdt] = 0;

	/*
	 * Set the new value in the watchdog.  Some versions of dw_wdt
	 * have TOPINIT in the TIMEOUT_RANGE register (as per
	 * CP_WDT_DUAL_TOP in WDT_COMP_PARAMS_1).  On those we
	 * effectively get a pat of the watchdog right here.
	 */
	reg = WDT_OFF(wdt_torr);
	writel(top_val | top_val << WDT_TIMEOUT_RANGE_TOPINIT_SHIFT, reg);

	return 0;
}

static void wdt_init_timeout(void)
{
	uint32_t reg;
	uint32_t val;

	reg = WDT_OFF(wdt_torr);
	val = readl(reg);
	val &= 0xF;

	if (wdt_rmod == DW_WDT_RMOD_IRQ)
		g_wdt_pretimeout[g_wdt] = wdt_timeouts[val].sec;
	else
		g_wdt_pretimeout[g_wdt] = 0;
}


static void wdt_init_tops(const uint32_t *tops)
{
	struct dw_wdt_timeout tout, *dst;
	uint32_t val, tidx;
	uint32_t msec;
	uint32_t clk = get_mod_clk_rate(CRG_MOD_WDT0);

	/*
	 * We walk over the passed TOPs array and calculate corresponding
	 * timeouts in seconds and milliseconds. The milliseconds granularity
	 * is needed to distinguish the TOPs with very close timeouts and to
	 * set the watchdog max heartbeat setting further.
	 */
	for (val = 0; val < WDT_NUM_TOPS; ++val) {
		tout.top_val = val;
		tout.sec = tops[val] / clk;
		msec = (uint64_t)tops[val] * MSEC_PER_SEC;
		DO_DIV(msec, clk);
		tout.msec = msec - ((uint64_t)tout.sec * MSEC_PER_SEC);

		/*
		 * Find a suitable place for the current TOP in the timeouts
		 * array so that the list is remained in the ascending order.
		 */
		for (tidx = 0; tidx < val; ++tidx) {
			dst = &wdt_timeouts[tidx];

			if (tout.sec > dst->sec || (tout.sec == dst->sec &&
			                            tout.msec >= dst->msec))
				continue;
			else
				swap(*dst, tout);
		}

		wdt_timeouts[val] = tout;
	}
}


static void dump_tops(void)
{
	uint32_t tidx;
	struct dw_wdt_timeout *tout;

	for (tidx = 0; tidx < WDT_NUM_TOPS; tidx++) {
		tout = &wdt_timeouts[tidx];
	}
}

static void wdt_enable(void)
{
	uint32_t reg;
	uint32_t val;

	reg = WDT_OFF(wdt_cr);
	val = readl(reg);

	/* enable watchdog. */
	val |= DW_WDT_CR_EN;
	writel(val, reg);
}

static void wdt_disable(void)
{
	uint32_t reg;
	uint32_t val;

	reg = WDT_OFF(wdt_cr);
	val = readl(reg);

	/* enable watchdog. */
	val &= ~DW_WDT_CR_EN;
	writel(val, reg);
}


void wdt_switch(enum wdt_port wdt)
{
	if (wdt < eDW_WDT_MAX)
		g_wdt = wdt;
}

void wdt_init(void)
{
	wdt_update_mode(DW_WDT_RMOD_IRQ);
	wdt_init_tops(wdt_fix_tops);
	wdt_stop();
	if (wdt_is_enabled())
		wdt_init_timeout();
}

void wdt_start(uint32_t timeout)
{
	wdt_set_timeout(timeout);
	wdt_enable();
	wdt_ping();
}

void wdt_stop(void)
{
	wdt_disable();
}

static uint32_t wdt_set_pretimeout(uint32_t req)
{
	wdt_update_mode(req ? DW_WDT_RMOD_IRQ : DW_WDT_RMOD_RESET);
	wdt_set_timeout(DW_WDT_DEF_PRETIMEOUT);

	return 0;
}

static uint32_t wdt_get_min_timeout(void)
{
	uint32_t idx;

	/*
	 * We'll find a timeout greater or equal to one second anyway because
	 * the driver probe would have failed if there was none.
	 */
	for (idx = 0; idx < WDT_NUM_TOPS; ++idx) {
		if (wdt_timeouts[idx].sec)
			break;
	}

	return wdt_timeouts[idx].sec;
}

static uint32_t wdt_get_max_timeout_ms(void)
{
	struct dw_wdt_timeout *timeout = &wdt_timeouts[WDT_NUM_TOPS - 1];
	uint64_t msec;

	msec = (uint64_t)timeout->sec * MSEC_PER_SEC + timeout->msec;

	return msec < UINT_MAX ? msec : UINT_MAX;
}

uint32_t wdt_get_timeout(void)
{
	uint32_t reg = WDT_OFF(wdt_torr);
	uint32_t top_val;
	uint32_t idx;

	top_val = readl(reg) & 0x0f;

	for (idx = 0; idx < WDT_NUM_TOPS; ++idx) {
		if (wdt_timeouts[idx].top_val == top_val)
			break;
	}

	/*
	 * In IRQ mode due to the two stages counter, the actual timeout is
	 * twice greater than the TOP setting.
	 */
	return wdt_timeouts[idx].sec * wdt_rmod;
}

void wdt_ping(void)
{
	uint32_t reg = WDT_OFF(wdt_crr);

	if (wdt_is_enabled())
		writel(DW_WDT_RESTART_KICK_VAL, reg);
}

uint32_t wdt_get_timeleft(void)
{
	uint32_t reg;
	uint32_t sec;
	uint32_t val;
	uint32_t clk = get_mod_clk_rate(CRG_MOD_WDT0);

	reg = WDT_OFF(wdt_ccvr);
	val = readl(reg);
	sec = val / clk;

	if (wdt_rmod == DW_WDT_RMOD_IRQ) {
		if (wdt_irq_flag == 0) {
			sec += wdt_pretimeout;
		}
	}

	return sec;
}

void wdt_isr(void *args)
{
	uint32_t reg;

	/*
	 * We don't clear the IRQ status. It's supposed to be done by the
	 * following ping operations.
	 */
	reg = WDT_OFF(wdt_stat);
	readl(reg);

	if (NULL != wdt_callback) {
		wdt_callback(args);
	}

	wdt_irq_flag = 1;

	reg = WDT_OFF(wdt_eoi);
	readl(reg);
}

void wdt_register_callback(DEV_CALLBACK callback)
{
	wdt_callback = callback;
}

